<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h1 data-lake-id="v88Fz" id="v88Fz"><span data-lake-id="u0d39ccc3" id="u0d39ccc3">典型回答</span></h1>
  <p data-lake-id="uda719c49" id="uda719c49"><br></p>
  <p data-lake-id="u04b2d7a7" id="u04b2d7a7"><span data-lake-id="ufd61d3a8" id="ufd61d3a8">分布式锁有多种实现方式，比较常见的实现是通过数据库、Redis或者Zookeeper来实现的。</span></p>
  <p data-lake-id="ue61ca52a" id="ue61ca52a"><br></p>
  <p data-lake-id="u1e89bf83" id="u1e89bf83"><span data-lake-id="u0c865a17" id="u0c865a17">其中数据库的实现可以依赖悲观锁以及数据库表记录来实现，通过Redis的实现可以考虑使用setnx、redission以及redlock实现。使用zk主要是依赖他提供的临时有序节点来实现。</span></p>
  <p data-lake-id="u3c4eb916" id="u3c4eb916"><br></p>
  <h1 data-lake-id="ib0nO" id="ib0nO"><span data-lake-id="u680d61b6" id="u680d61b6">扩展知识</span></h1>
  <p data-lake-id="u5386c34c" id="u5386c34c"><br></p>
  <p data-lake-id="ud7c37fb8" id="ud7c37fb8"><span data-lake-id="u2950feb1" id="u2950feb1">在分析这几种实现方案之前我们先来想一下，我们需要的分布式锁应该是怎么样的？</span></p>
  <p data-lake-id="u0a677461" id="u0a677461"><br></p>
  <blockquote data-lake-id="u891c5083" id="u891c5083">
   <p data-lake-id="u67898508" id="u67898508"><span data-lake-id="u6e5382b0" id="u6e5382b0">可以保证在分布式部署的应用集群中，同一个方法在同一时间只能被一台机器上的一个线程执行。</span></p>
   <p data-lake-id="u37e1a639" id="u37e1a639"><span data-lake-id="uff84f02a" id="uff84f02a"> </span></p>
   <p data-lake-id="u6659a5cf" id="u6659a5cf"><span data-lake-id="u2cb6eefa" id="u2cb6eefa">这把锁要是一把可重入锁（避免死锁）</span></p>
   <p data-lake-id="ud6435c55" id="ud6435c55"><span data-lake-id="u667a0091" id="u667a0091"> </span></p>
   <p data-lake-id="u282e790b" id="u282e790b"><span data-lake-id="u23409b42" id="u23409b42">这把锁最好是一把阻塞锁（根据业务需求考虑要不要这条）</span></p>
   <p data-lake-id="u13e033b1" id="u13e033b1"><span data-lake-id="uadd4bffb" id="uadd4bffb"> </span></p>
   <p data-lake-id="ua789cdae" id="ua789cdae"><span data-lake-id="u5e5451bf" id="u5e5451bf">有高可用的获取锁和释放锁功能</span></p>
   <p data-lake-id="u686018eb" id="u686018eb"><span data-lake-id="u237b59c7" id="u237b59c7"> </span></p>
   <p data-lake-id="u9da45e4b" id="u9da45e4b"><span data-lake-id="u0e3384f9" id="u0e3384f9">获取锁和释放锁的性能要好</span></p>
  </blockquote>
  <h2 data-lake-id="yt4SV" id="yt4SV"><span data-lake-id="ufc991ad0" id="ufc991ad0">基于数据库实现分布式锁</span></h2>
  <h3 data-lake-id="FQUqk" id="FQUqk"><span data-lake-id="u70f37fe5" id="u70f37fe5">基于数据库表</span></h3>
  <p data-lake-id="ua6e6b054" id="ua6e6b054"><br></p>
  <p data-lake-id="u7d87aaae" id="u7d87aaae"><span data-lake-id="ua6f986df" id="ua6f986df">要实现分布式锁，最简单的方式可能就是直接创建一张锁表，然后通过操作该表中的数据来实现了。</span></p>
  <p data-lake-id="u07561116" id="u07561116"><br></p>
  <p data-lake-id="u545d34fe" id="u545d34fe"><span data-lake-id="u657513d2" id="u657513d2">当我们要锁住某个方法或资源时，我们就在该表中增加一条记录，想要释放锁的时候就删除这条记录。</span></p>
  <p data-lake-id="u2e69c31b" id="u2e69c31b"><br></p>
  <p data-lake-id="ucf303035" id="ucf303035"><span data-lake-id="u9574f9b6" id="u9574f9b6">创建这样一张数据库表：</span></p>
  <p data-lake-id="u4ac2f193" id="u4ac2f193"><br></p>
  <pre lang="java"><code>
CREATE TABLE `methodLock` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁定的方法名',
  `desc` varchar(1024) NOT NULL DEFAULT '备注信息',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间，自动生成',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';
</code></pre>
  <p data-lake-id="u3ad66646" id="u3ad66646"><br></p>
  <p data-lake-id="uc3f53ffa" id="uc3f53ffa"><span data-lake-id="ue0ffb414" id="ue0ffb414">当我们想要锁住某个方法时，执行以下SQL：</span></p>
  <p data-lake-id="u2d809ac4" id="u2d809ac4"><br></p>
  <pre lang="java"><code>
insert into methodLock(method_name,desc) values (‘method_name’,‘desc’)
</code></pre>
  <p data-lake-id="u9be6a2a0" id="u9be6a2a0"><br></p>
  <p data-lake-id="u46652e0b" id="u46652e0b"><span data-lake-id="u55199e8a" id="u55199e8a">因为我们对</span><code data-lake-id="ueb29bb83" id="ueb29bb83"><span data-lake-id="u341648e6" id="u341648e6">method_name</span></code><span data-lake-id="uc99689bb" id="uc99689bb">做了唯一性约束，这里如果有多个请求同时提交到数据库的话，数据库会保证只有一个操作可以成功，那么我们就可以认为操作成功的那个线程获得了该方法的锁，可以执行方法体内容。</span></p>
  <p data-lake-id="udc2d6782" id="udc2d6782"><br></p>
  <p data-lake-id="ud0bde744" id="ud0bde744"><span data-lake-id="u948f731d" id="u948f731d">当方法执行完毕之后，想要释放锁的话，需要执行以下Sql:</span></p>
  <p data-lake-id="u088e7195" id="u088e7195"><br></p>
  <pre lang="java"><code>
delete from methodLock where method_name ='method_name'
</code></pre>
  <p data-lake-id="ucf7099fc" id="ucf7099fc"><br></p>
  <p data-lake-id="u172a1628" id="u172a1628"><span data-lake-id="ucbdc6d9a" id="ucbdc6d9a">上面这种简单的实现有以下几个问题：</span></p>
  <p data-lake-id="ucbef81c4" id="ucbef81c4"><br></p>
  <blockquote data-lake-id="u5e4ff113" id="u5e4ff113">
   <p data-lake-id="u1a035ef1" id="u1a035ef1"><span data-lake-id="u0f6c9973" id="u0f6c9973">1、这把锁强依赖数据库的可用性，数据库是一个单点，一旦数据库挂掉，会导致业务系统不可用。</span></p>
   <p data-lake-id="ufad9a1d4" id="ufad9a1d4"><span data-lake-id="u17dfb7e4" id="u17dfb7e4"> </span></p>
   <p data-lake-id="ua07f890b" id="ua07f890b"><span data-lake-id="u856d0635" id="u856d0635">2、这把锁没有失效时间，一旦解锁操作失败，就会导致锁记录一直在数据库中，其他线程无法再获得到锁。</span></p>
   <p data-lake-id="ub1f35bf7" id="ub1f35bf7"><span data-lake-id="uf0cd1031" id="uf0cd1031"> </span></p>
   <p data-lake-id="u2dafa874" id="u2dafa874"><span data-lake-id="u197837ed" id="u197837ed">3、这把锁只能是非阻塞的，因为数据的insert操作，一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列，要想再次获得锁就要再次触发获得锁操作。</span></p>
   <p data-lake-id="u04079a52" id="u04079a52"><span data-lake-id="u7585db73" id="u7585db73"> </span></p>
   <p data-lake-id="u9038ffba" id="u9038ffba"><span data-lake-id="u014a6a5a" id="u014a6a5a">4、这把锁是非重入的，同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。</span></p>
  </blockquote>
  <p data-lake-id="u37f6d3a7" id="u37f6d3a7"><br></p>
  <p data-lake-id="u7ad2da5f" id="u7ad2da5f"><span data-lake-id="u8354bd9a" id="u8354bd9a">当然，我们也可以有其他方式解决上面的问题。</span></p>
  <p data-lake-id="uc6ffe23a" id="uc6ffe23a"><br></p>
  <ul list="uba5d3479">
   <li fid="u436ebd85" data-lake-id="u80927f4f" id="u80927f4f"><span data-lake-id="u05853ede" id="u05853ede">数据库是单点？搞两个数据库，数据之前双向同步。一旦挂掉快速切换到备库上。</span></li>
   <li fid="u436ebd85" data-lake-id="ue52a55f4" id="ue52a55f4"><span data-lake-id="u37efe73b" id="u37efe73b">没有失效时间？只要做一个定时任务，每隔一定时间把数据库中的超时数据清理一遍。</span></li>
   <li fid="u436ebd85" data-lake-id="u3199e3fd" id="u3199e3fd"><span data-lake-id="u0caae1d1" id="u0caae1d1">非阻塞的？搞一个while循环，直到insert成功再返回成功。</span></li>
   <li fid="u436ebd85" data-lake-id="uec028035" id="uec028035"><span data-lake-id="u64dc52b2" id="u64dc52b2">非重入的？在数据库表中加个字段，记录当前获得锁的机器的主机信息和线程信息，那么下次再获取锁的时候先查询数据库，如果当前机器的主机信息和线程信息在数据库可以查到的话，直接把锁分配给他就可以了。</span></li>
  </ul>
  <p data-lake-id="u7e331681" id="u7e331681"><br></p>
  <h3 data-lake-id="CJQP3" id="CJQP3"><span data-lake-id="u80440a1b" id="u80440a1b">基于数据库排他锁</span></h3>
  <p data-lake-id="ube19cf79" id="ube19cf79"><br></p>
  <p data-lake-id="u402b524e" id="u402b524e"><span data-lake-id="ua0dc2ddf" id="ua0dc2ddf">除了可以通过增删操作数据表中的记录以外，其实还可以借助数据中自带的锁来实现分布式的锁。</span></p>
  <p data-lake-id="uede8ca9b" id="uede8ca9b"><br></p>
  <p data-lake-id="u9f9927d4" id="u9f9927d4"><span data-lake-id="ud30ca74a" id="ud30ca74a">我们还用刚刚创建的那张数据库表。可以通过数据库的排他锁来实现分布式锁。 基于MySql的InnoDB引擎，可以使用以下方法来实现加锁操作：</span></p>
  <p data-lake-id="u60df1d3f" id="u60df1d3f"><br></p>
  <pre lang="java"><code>
public boolean lock(){
    connection.setAutoCommit(false)
    while(true){
        try{
            result = select * from methodLock where method_name=xxx for update;
            if(result==null){
                return true;
            }
        }catch(Exception e){

        }
        sleep(1000);
    }
    return false;
}
</code></pre>
  <p data-lake-id="uc8509fb6" id="uc8509fb6"><br></p>
  <p data-lake-id="u1936a121" id="u1936a121"><span data-lake-id="ud23e0a06" id="ud23e0a06">在查询语句后面增加</span><code data-lake-id="u8e8ac0c5" id="u8e8ac0c5"><span data-lake-id="ub1979fd7" id="ub1979fd7">for update</span></code><span data-lake-id="ub414066d" id="ub414066d">，数据库会在查询过程中给数据库表增加排他锁（这里再多提一句，InnoDB引擎在加锁的时候，只有通过索引进行检索的时候才会使用行级锁，否则会使用表级锁。这里我们希望使用行级锁，就要给method_name添加索引，值得注意的是，这个索引一定要创建成唯一索引，否则会出现多个重载方法之间无法同时被访问的问题。重载方法的话建议把参数类型也加上。）。当某条记录被加上排他锁之后，其他线程无法再在该行记录上增加排他锁。</span></p>
  <p data-lake-id="u01a31cd9" id="u01a31cd9"><br></p>
  <p data-lake-id="ub94650bc" id="ub94650bc"><span data-lake-id="u20e4f6af" id="u20e4f6af">我们可以认为获得排它锁的线程即可获得分布式锁，当获取到锁之后，可以执行方法的业务逻辑，执行完方法之后，再通过以下方法解锁：</span></p>
  <p data-lake-id="ud107caee" id="ud107caee"><br></p>
  <pre lang="java"><code>
public void unlock(){
    connection.commit();
}
</code></pre>
  <p data-lake-id="u1777b9a9" id="u1777b9a9"><br></p>
  <p data-lake-id="ub424a621" id="ub424a621"><span data-lake-id="u44b7573f" id="u44b7573f">通过</span><code data-lake-id="uf14b8822" id="uf14b8822"><span data-lake-id="udf118e8c" id="udf118e8c">connection.commit()</span></code><span data-lake-id="ue7443bc1" id="ue7443bc1">操作来释放锁。</span></p>
  <p data-lake-id="u6584f71f" id="u6584f71f"><br></p>
  <p data-lake-id="ub547cdc7" id="ub547cdc7"><span data-lake-id="u662798b3" id="u662798b3">这种方法可以有效的解决上面提到的无法释放锁和阻塞锁的问题。</span></p>
  <p data-lake-id="ucc97016e" id="ucc97016e"><br></p>
  <ul list="u49435124">
   <li fid="u9406da17" data-lake-id="u8c8b44d7" id="u8c8b44d7"><span data-lake-id="u2044617e" id="u2044617e">阻塞锁？ </span><code data-lake-id="u907df801" id="u907df801"><span data-lake-id="u8f8a892c" id="u8f8a892c">for update</span></code><span data-lake-id="u43b85a18" id="u43b85a18">语句会在执行成功后立即返回，在执行失败时一直处于阻塞状态，直到成功。</span></li>
   <li fid="u9406da17" data-lake-id="u592e286c" id="u592e286c"><span data-lake-id="u9621bb18" id="u9621bb18">锁定之后服务宕机，无法释放？使用这种方式，服务宕机之后数据库会自己把锁释放掉。</span></li>
  </ul>
  <p data-lake-id="u510031a3" id="u510031a3"><br></p>
  <p data-lake-id="u976e5ea1" id="u976e5ea1"><span data-lake-id="u8c2e44db" id="u8c2e44db">但是还是无法直接解决数据库单点和可重入问题。</span></p>
  <p data-lake-id="ue7e9e30d" id="ue7e9e30d"><br></p>
  <p data-lake-id="u2e4cb3b3" id="u2e4cb3b3"><strong><span data-lake-id="uae459f84" id="uae459f84">这里还可能存在另外一个问题，虽然我们对</span></strong><code data-lake-id="ua8c3fac0" id="ua8c3fac0"><strong><span data-lake-id="uff10712e" id="uff10712e">method_name</span></strong></code><strong><span data-lake-id="u8d3a71e7" id="u8d3a71e7"> 使用了唯一索引，并且显示使用</span></strong><code data-lake-id="uafd3b0f2" id="uafd3b0f2"><strong><span data-lake-id="ufb325d27" id="ufb325d27">for update</span></strong></code><strong><span data-lake-id="ue972fbf3" id="ue972fbf3">来使用行级锁。但是，MySql会对查询进行优化，即便在条件中使用了索引字段，但是否使用索引来检索数据是由 MySQL 通过判断不同执行计划的代价来决定的，如果 MySQL 认为全表扫效率更高，比如对一些很小的表，它就不会使用索引，这种情况下 InnoDB 将使用表锁，而不是行锁。如果发生这种情况就悲剧了。。。</span></strong></p>
  <p data-lake-id="u1c79ec40" id="u1c79ec40"><br></p>
  <p data-lake-id="uacac0383" id="uacac0383"><strong><span data-lake-id="u0a393039" id="u0a393039">还有一个问题，就是我们要使用排他锁来进行分布式锁的lock，那么一个排他锁长时间不提交，就会占用数据库连接。一旦类似的连接变得多了，就可能把数据库连接池撑爆</span></strong></p>
  <p data-lake-id="u0bdb6d04" id="u0bdb6d04"><br></p>
  <h3 data-lake-id="VRtnk" id="VRtnk"><span data-lake-id="ue41e5ee3" id="ue41e5ee3">数据库实现分布式锁的优缺点</span></h3>
  <p data-lake-id="u3a70d2a1" id="u3a70d2a1"><br></p>
  <p data-lake-id="ud9e20c82" id="ud9e20c82"><span data-lake-id="u67d90be3" id="u67d90be3">总结一下使用数据库来实现分布式锁的方式，这两种方式都是依赖数据库的一张表，一种是通过表中的记录的存在情况确定当前是否有锁存在，另外一种是通过数据库的排他锁来实现分布式锁。</span></p>
  <p data-lake-id="u611e1973" id="u611e1973"><br></p>
  <p data-lake-id="u3a522a54" id="u3a522a54"><strong><span data-lake-id="u7e1beae9" id="u7e1beae9">数据库实现分布式锁的优点</span></strong></p>
  <p data-lake-id="u3e9fd319" id="u3e9fd319"><br></p>
  <p data-lake-id="u94cd693c" id="u94cd693c"><span data-lake-id="u1427ea40" id="u1427ea40">直接借助数据库，容易理解。</span></p>
  <p data-lake-id="u70970b30" id="u70970b30"><br></p>
  <p data-lake-id="uf3ee15de" id="uf3ee15de"><strong><span data-lake-id="u980a6c30" id="u980a6c30">数据库实现分布式锁的缺点</span></strong></p>
  <p data-lake-id="u5492056d" id="u5492056d"><br></p>
  <p data-lake-id="u64db3e5f" id="u64db3e5f"><span data-lake-id="ufa7b95e4" id="ufa7b95e4">会有各种各样的问题，在解决问题的过程中会使整个方案变得越来越复杂。</span></p>
  <p data-lake-id="ueb69c11f" id="ueb69c11f"><br></p>
  <p data-lake-id="ud797a2f5" id="ud797a2f5"><span data-lake-id="uf7c2bce9" id="uf7c2bce9">操作数据库需要一定的开销，性能问题需要考虑。</span></p>
  <p data-lake-id="ude2bcb9f" id="ude2bcb9f"><br></p>
  <p data-lake-id="u2afe1a78" id="u2afe1a78"><span data-lake-id="ube96cb22" id="ube96cb22">使用数据库的行级锁并不一定靠谱，尤其是当我们的锁表并不大的时候。</span></p>
  <p data-lake-id="u5477a103" id="u5477a103"><br></p>
  <h2 data-lake-id="KUQ31" id="KUQ31"><span data-lake-id="ua1d83a76" id="ua1d83a76">基于缓存实现分布式锁</span></h2>
  <p data-lake-id="ua7c22e22" id="ua7c22e22"><br></p>
  <p data-lake-id="u8da771f3" id="u8da771f3"><span data-lake-id="u419195da" id="u419195da">相比较于基于数据库实现分布式锁的方案来说，基于缓存来实现在性能方面会表现的更好一点。而且很多缓存是可以集群部署的，可以解决单点问题。</span></p>
  <p data-lake-id="u6be227e1" id="u6be227e1"><br></p>
  <p data-lake-id="ube5fe593" id="ube5fe593"><span data-lake-id="uf1b5755b" id="uf1b5755b">目前有很多成熟的缓存产品，包括Redis，memcached以及我们公司内部的Tair。</span></p>
  <p data-lake-id="u92cdbf1a" id="u92cdbf1a"><br></p>
  <p data-lake-id="u80e10e0a" id="u80e10e0a"><span data-lake-id="uf12a6a6e" id="uf12a6a6e">关于Redis实现分布式锁的细节，详见：</span></p>
  <p data-lake-id="u33957964" id="u33957964"><span data-lake-id="u311473c7" id="u311473c7">​</span><br></p>
  <p data-lake-id="u6ac3b7aa" id="u6ac3b7aa"><br></p>
  <p data-lake-id="u3d15417e" id="u3d15417e"><br></p>
  <p data-lake-id="u954f769e" id="u954f769e"><br></p>
  <h2 data-lake-id="Z03xm" id="Z03xm"><span data-lake-id="ue0d9794f" id="ue0d9794f">基于Zookeeper实现分布式锁</span></h2>
  <p data-lake-id="u17d38286" id="u17d38286"><br></p>
  <p data-lake-id="u3d70bcc6" id="u3d70bcc6"><span data-lake-id="u96abe2d3" id="u96abe2d3">基于zookeeper临时有序节点可以实现的分布式锁。</span></p>
  <p data-lake-id="ub155d35d" id="ub155d35d"><br></p>
  <p data-lake-id="u7f42824c" id="u7f42824c"><span data-lake-id="u388b5fdc" id="u388b5fdc">大致思想即为：每个客户端对某个方法加锁时，在zookeeper上的与该方法对应的指定节点的目录下，生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单，只需要判断有序节点中序号最小的一个。 当释放锁的时候，只需将这个瞬时节点删除即可。同时，其可以避免服务宕机导致的锁无法释放，而产生的死锁问题。</span></p>
  <p data-lake-id="u64cf7d3a" id="u64cf7d3a"><br></p>
  <p data-lake-id="u96aed191" id="u96aed191"><span data-lake-id="ua19595b8" id="ua19595b8"><br><br></span></p>
 </body>
</html>